package com.gambi.tapestry.services.gwt;

/*
 * This class is taken and patched form the original OnEventWorker. It checks the GWT annotations on methods marked via OnEvent or the ones that match the naming convention
 */
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

import org.apache.tapestry5.EventContext;
import org.apache.tapestry5.ValueEncoder;
import org.apache.tapestry5.annotations.OnEvent;
import org.apache.tapestry5.annotations.RequestParameter;
import org.apache.tapestry5.func.Predicate;
import org.apache.tapestry5.internal.services.ComponentClassCache;
import org.apache.tapestry5.internal.transform.BaseEventHandlerMethodInvoker;
import org.apache.tapestry5.internal.transform.EventHandlerMethodInvoker;
import org.apache.tapestry5.internal.transform.EventHandlerMethodParameterSource;
import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
import org.apache.tapestry5.ioc.internal.util.InternalUtils;
import org.apache.tapestry5.model.MutableComponentModel;
import org.apache.tapestry5.runtime.ComponentEvent;
import org.apache.tapestry5.services.ClassTransformation;
import org.apache.tapestry5.services.ComponentClassTransformWorker;
import org.apache.tapestry5.services.ComponentMethodAdvice;
import org.apache.tapestry5.services.ComponentMethodInvocation;
import org.apache.tapestry5.services.Request;
import org.apache.tapestry5.services.RequestGlobals;
import org.apache.tapestry5.services.TransformConstants;
import org.apache.tapestry5.services.TransformMethod;
import org.apache.tapestry5.services.TransformMethodSignature;
import org.apache.tapestry5.services.ValueEncoderSource;

import com.gambi.tapestry.utils.GWTConstant;
import com.gambi.tapestry.utils.GWTNull;
import com.gambi.tapestry.utils.GWTUtils;
import com.gambi.tapestry.utils.GWTVoid;
import com.google.gwt.user.server.rpc.RPCRequest;
import com.trsvax.tapestry.gwt.annotation.GWT;

public class GWTOnEventWorker implements ComponentClassTransformWorker {

	private final Request request;

	private final ValueEncoderSource valueEncoderSource;

	private final ComponentClassCache classCache;

	private final List<TransformMethod> blockingMethods = new ArrayList<TransformMethod>();

	private final GWTComponentEventSource gwtComponentEvent;
	/**
	 * Stores a couple of special parameter type mappings that are used when
	 * matching the entire event context (either as Object[] or EventContext).
	 */
	private final Map<String, EventHandlerMethodParameterSource> parameterTypeToSource = CollectionFactory
			.newMap();

	{
		// Object[] and List are out-dated and may be deprecated some day

		parameterTypeToSource.put("java.lang.Object[]",
				new EventHandlerMethodParameterSource() {

					public Object valueForEventHandlerMethodParameter(
							ComponentEvent event) {
						return event.getContext();
					}
				});

		parameterTypeToSource.put(List.class.getName(),
				new EventHandlerMethodParameterSource() {

					public Object valueForEventHandlerMethodParameter(
							ComponentEvent event) {
						return Arrays.asList(event.getContext());
					}
				});

		// This is better, as the EventContext maintains the original objects
		// (or strings)
		// and gives the event handler method access with coercion
		parameterTypeToSource.put(EventContext.class.getName(),
				new EventHandlerMethodParameterSource() {

					public Object valueForEventHandlerMethodParameter(
							ComponentEvent event) {
						return event.getEventContext();
					}
				});
	}

	public GWTOnEventWorker(Request request,
			ValueEncoderSource valueEncoderSource,
			ComponentClassCache classCache,
			GWTComponentEventSource gwtComponentEvent) {
		this.request = request;
		this.valueEncoderSource = valueEncoderSource;
		this.classCache = classCache;
		this.gwtComponentEvent = gwtComponentEvent;
	}

	public void transform(ClassTransformation transformation,
			MutableComponentModel model) {

		// List all the matching event methods. Same logic as before
		List<TransformMethod> methods = matchEventHandlerMethods(transformation);

		if (methods.isEmpty()) {
			return;
		}

		// Filter and restructure the set according the GWT/Annotations. If any.
		// We consider only the eventHandler methods

		for (TransformMethod method : methods) {
			GWT annotation = method.getAnnotation(GWT.class);
			if (annotation == null) {
				continue;
			} else {
				if (!annotation.bubbling()) {
					// store the reference for this method
					blockingMethods.add(method);
				}
			}
		}

		List<EventHandlerMethodInvoker> invokers = toInvokers(
				transformation.getClassName(), methods);

		updateModelWithHandledEvents(model, invokers);

		adviseDispatchComponentEventMethod(transformation, invokers);
	}

	private void adviseDispatchComponentEventMethod(
			ClassTransformation transformation,
			List<EventHandlerMethodInvoker> invokers) {
		ComponentMethodAdvice advice = createDispatchComponentEventAdvice(invokers);

		transformation.getOrCreateMethod(
				TransformConstants.DISPATCH_COMPONENT_EVENT).addAdvice(advice);
	}

	private ComponentMethodAdvice createDispatchComponentEventAdvice(
			final List<EventHandlerMethodInvoker> invokers) {

		return new ComponentMethodAdvice() {
			public void advise(ComponentMethodInvocation invocation) {
				// Invoke the super-class implementation first. If no
				// super-class,
				// this will do nothing and return false.

				invocation.proceed();

				ComponentEvent event = (ComponentEvent) invocation
						.getParameter(0);

				if (invokeEventHandlers(event, invocation.getInstance())) {
					System.out
							.println("GWTOnEventWorker.createDispatchComponentEventAdvice(...).new ComponentMethodAdvice() {...}.advise()"
									+ " OVERRIDE RESULT ");
					invocation.overrideResult(true);
				}
			}

			private boolean invokeEventHandlers(ComponentEvent event,
					Object instance) {
				// If the super-class aborted the event (some super-class method
				// return non-null),
				// then it's all over, don't even check for handlers in this
				// class.

				if (event.isAborted()) {
					return false;
				}

				boolean didInvokeSomeHandler = false;

				for (EventHandlerMethodInvoker invoker : invokers) {
					System.out.println(String.format("%s %s %s",
							invoker.getEventType(), invoker.getComponentId(),
							invoker.getMinContextValueCount()));

					// Seems not to work properly ...
					if (event.matches(invoker.getEventType(),
							invoker.getComponentId(),
							invoker.getMinContextValueCount())) {

						didInvokeSomeHandler = true;

						if (invoker instanceof GWTBaseEventHandlerMethodInvoker) {

							GWTBaseEventHandlerMethodInvoker gwtInvoker = (GWTBaseEventHandlerMethodInvoker) invoker;

							if (gwtInvoker.isBlocking()) {
								System.out
										.println("GWTOnEventWorker.createDispatchComponentEventAdvice(...).new ComponentMethodAdvice() {...}.invokeEventHandlers()  BLOCKING METHOD !");

								request.setAttribute("BLOCKING", true);
								System.out
										.println("GWTOnEventWorker.createDispatchComponentEventAdvice(...).new ComponentMethodAdvice() {...}.invokeEventHandlers() Return type "
												+ gwtInvoker.getReturnType());

								if ("void".equals(gwtInvoker.getReturnType())
										|| "boolean".equals(gwtInvoker
												.getReturnType())) {
									event = new GWTWrappedComponentEvent(event);
								} else {
									System.out
											.println("NORMAL OBJECTS BLOCK BUBBLING ANYWAY !");
								}

								invoker.invokeEventHandlerMethod(event,
										instance);

								if ("void".equals(gwtInvoker.getReturnType())) {
									event.storeResult(new GWTVoid());
								} else {
									if (!event.isAborted()) {
										System.out
												.println("Null Return Value !");
										event.storeResult(new GWTNull());
									}
								}
							} else {
								System.out
										.println("GWTOnEventWorker.createDispatchComponentEventAdvice(...).new ComponentMethodAdvice() {...}.invokeEventHandlers()  NORMAL METHOD !");
								invoker.invokeEventHandlerMethod(event,
										instance);
							}
						} else {
							System.out
									.println("GWTOnEventWorker.createDispatchComponentEventAdvice(...).new ComponentMethodAdvice() {...}.invokeEventHandlers()  NORMAL METHOD, NORMAL Handler!");
							invoker.invokeEventHandlerMethod(event, instance);
						}

						if (event.isAborted()) {
							break;
						}
					}
				}

				return didInvokeSomeHandler;
			}
		};
	}

	private void updateModelWithHandledEvents(MutableComponentModel model,
			final List<EventHandlerMethodInvoker> invokers) {
		String componentClassName = model.getComponentClassName();
		System.out.println("GWTOnEventWorker.updateModelWithHandledEvents() "
				+ model.getComponentClassName());
		gwtComponentEvent.registerComponent(componentClassName);
		for (EventHandlerMethodInvoker invoker : invokers) {
			model.addEventHandler(invoker.getEventType());
			System.out.println(invoker.getEventType());
			gwtComponentEvent.addEventToComponent(componentClassName, invoker
					.getEventType().toLowerCase());
		}
	}

	private List<TransformMethod> matchEventHandlerMethods(
			ClassTransformation transformation) {
		return transformation.matchMethods(new Predicate<TransformMethod>() {
			public boolean accept(TransformMethod method) {
				return (hasCorrectPrefix(method) || hasAnnotation(method))
						&& !method.isOverride();
			}

			private boolean hasCorrectPrefix(TransformMethod method) {
				return method.getName().startsWith("on");
			}

			private boolean hasAnnotation(TransformMethod method) {
				return method.getAnnotation(OnEvent.class) != null;
			}
		});
	}

	private List<EventHandlerMethodInvoker> toInvokers(
			String componentClassName, List<TransformMethod> methods) {
		List<EventHandlerMethodInvoker> result = CollectionFactory.newList();

		for (TransformMethod method : methods) {
			result.add(toInvoker(componentClassName, method));
		}

		return result;
	}

	private EventHandlerMethodInvoker toInvoker(
			final String componentClassName, TransformMethod method) {

		System.out.println("GWTOnEventWorker.toInvoker() "
				+ method.getSignature());

		OnEvent annotation = method.getAnnotation(OnEvent.class);

		String methodName = method.getName();

		String eventType = extractEventType(methodName, annotation);
		String componentId = extractComponentId(methodName, annotation);

		final TransformMethodSignature signature = method.getSignature();

		String[] parameterTypes = signature.getParameterTypes();

		if (parameterTypes.length == 0) {
			return new GWTBaseEventHandlerMethodInvoker(method, eventType,
					componentId);
			// return new BaseEventHandlerMethodInvoker(method, eventType,
			// componentId);
		}

		final List<EventHandlerMethodParameterSource> sources = CollectionFactory
				.newList();

		// I'd refactor a bit more of this if Java had covariant return types.

		int contextIndex = 0;

		for (int i = 0; i < parameterTypes.length; i++) {
			String type = parameterTypes[i];

			EventHandlerMethodParameterSource source = parameterTypeToSource
					.get(type);

			if (source != null) {
				sources.add(source);
				continue;
			}

			RequestParameter parameterAnnotation = method
					.getParameterAnnotation(i, RequestParameter.class);

			if (parameterAnnotation != null) {
				String parameterName = parameterAnnotation.value();

				sources.add(createQueryParameterSource(componentClassName,
						signature, i, parameterName, type,
						parameterAnnotation.allowBlank()));
				continue;
			}

			// Note: probably safe to do the conversion to Class early (class
			// load time)
			// as parameters are rarely (if ever) component classes.

			final int parameterIndex = contextIndex++;

			sources.add(createEventContextSource(type, parameterIndex));
		}

		return createInvoker(method, eventType, componentId, contextIndex,
				sources);
	}

	// TODO Modified version for GWT Component Request Parameters
	private EventHandlerMethodParameterSource createQueryParameterSource(
			final String componentClassName,
			final TransformMethodSignature signature,

			final int parameterIndex, final String parameterName,
			final String parameterTypeName,

			final boolean allowBlank) {

		return new EventHandlerMethodParameterSource() {
			@SuppressWarnings("unchecked")
			public Object valueForEventHandlerMethodParameter(
					ComponentEvent event) {

				// THIS IS THE ADDITION FOR GWT RELATED PARAMETER COMPONENTS ...

				// GWT Related
				// TODO Do nothing for the moment
				try {
					if (GWTUtils.isGwtEncryptedRequest(request)) {
						// Passed via Annotation
						System.out
								.println("GWTOnEventWorker.createQueryParameterSource(...).new EventHandlerMethodParameterSource() {...}.valueForEventHandlerMethodParameter( )"
										+ parameterName);
						if (parameterName
								.startsWith(GWTConstant.GWT_REQUEST_PARAMETER_RELATIVE_PREFIX)) {
							String pName = parameterName
									.replaceAll(
											GWTConstant.GWT_REQUEST_PARAMETER_RELATIVE_PREFIX,
											"");
							System.out
									.println("GWTOnEventWorker.createQueryParameterSource(...) "
											+ pName);

							// DO
							System.out
									.println("GWTOnEventWorker.createQueryParameterSource(...) Relative Position");

							// Type:RelativePosition
							int index = Integer.parseInt(pName);

							System.out
									.println("GWTOnEventWorker.createQueryParameterSource(...).new EventHandlerMethodParameterSource() {...}.valueForEventHandlerMethodParameter( )"
											+ parameterTypeName);
							System.out
									.println("GWTOnEventWorker.createQueryParameterSource(...).new EventHandlerMethodParameterSource() {...}.valueForEventHandlerMethodParameter( )"
											+ index);

							// Now return the indexTH parameter of Type type
							Class[] parameterTypes = ((RPCRequest) request
									.getAttribute(RPCRequest.class.getName()))
									.getMethod().getParameterTypes();
							Class localParameterType = Class
									.forName(parameterTypeName);

							for (int i = 0; i < parameterTypes.length; i++) {
								// if (parameterTypes[i].getName().equals(type))
								// {
								if (localParameterType
										.isAssignableFrom(parameterTypes[i])) {
									index--;
									if (index == -1) {
										return ((RPCRequest) request
												.getAttribute(RPCRequest.class
														.getName()))
												.getParameters()[i];
									}
								}
							}
							return null;
						} else if (parameterName
								.startsWith(GWTConstant.GWT_REQUEST_PARAMETER_ABSOLUTE_PREFIX)) {
							String pName = parameterName
									.replaceAll(
											GWTConstant.GWT_REQUEST_PARAMETER_ABSOLUTE_PREFIX,
											"");
							System.out
									.println("GWTOnEventWorker.createQueryParameterSource(...).new EventHandlerMethodParameterSource() {...}.valueForEventHandlerMethodParameter( )"
											+ pName);

							// AbsolutePosition

							// Take the index out of the gwt parameter name
							int index = Integer.parseInt(pName);
							Object value = ((RPCRequest) request
									.getAttribute(RPCRequest.class.getName()))
									.getParameters()[index];
							if (Class.forName(parameterTypeName)
									.isAssignableFrom(value.getClass())) {
								return value;
							}
							throw new RuntimeException(
									String.format(
											"GWT OnEvent: the provided type %s is not assignable from the actual type %s ",
											parameterTypeName, value.getClass()
													.getName()));
						} else {
							System.out
									.println("GWTOnEventWorker.createQueryParameterSource(...).new EventHandlerMethodParameterSource() {...}.valueForEventHandlerMethodParameter() "
											+ "NOT A GWT REQUEST PARAMETER");
						}
					}
				} catch (Exception ex) {
					throw new RuntimeException(
							String.format(
									"Unable process query parameter '%s' as parameter #%d of event handler method %s (in class %s): %s",
									parameterName, parameterIndex + 1,
									signature, componentClassName,
									InternalUtils.toMessage(ex)), ex);
				}

				// Traditional Tapestry

				try {

					String parameterValue = request.getParameter(parameterName);
					if (!allowBlank && InternalUtils.isBlank(parameterValue))
						throw new RuntimeException(
								String.format(
										"The value for query parameter '%s' was blank, but a non-blank value is needed.",
										parameterName));

					Class parameterType = classCache.forName(parameterTypeName);

					ValueEncoder valueEncoder = valueEncoderSource
							.getValueEncoder(parameterType);

					Object value = valueEncoder.toValue(parameterValue);

					if (parameterType.isPrimitive() && value == null)
						throw new RuntimeException(
								String.format(
										"Query parameter '%s' evaluates to null, but the event method parameter is type %s, a primitive.",
										parameterName, parameterType.getName()));

					return value;
				} catch (Exception ex) {
					throw new RuntimeException(
							String.format(
									"Unable process query parameter '%s' as parameter #%d of event handler method %s (in class %s): %s",
									parameterName, parameterIndex + 1,
									signature, componentClassName,
									InternalUtils.toMessage(ex)), ex);
				}
			}
		};
	}

	public class GWTBaseEventHandlerMethodInvoker extends
			BaseEventHandlerMethodInvoker {
		private boolean blocking = false;
		private String returnType;

		public GWTBaseEventHandlerMethodInvoker(TransformMethod method,
				String eventType, String componentId) {

			super(method, eventType, componentId);
			System.out.println("GWTOnEventWorker for " + method.getSignature());

			this.blocking = blockingMethods.contains(method);
			System.out.println(method.getName() + " is Blocking ?" + blocking);

			// Return type
			this.returnType = method.getSignature().getReturnType();
			System.out.println(method.getName() + " return " + returnType);
		}

		public boolean isBlocking() {
			return blocking;
		}

		public String getReturnType() {
			return returnType;
		}

	}

	private EventHandlerMethodInvoker createInvoker(
			final TransformMethod method, String eventType, String componentId,
			final int minContextCount,
			final List<EventHandlerMethodParameterSource> sources) {

		return new GWTBaseEventHandlerMethodInvoker(method, eventType,
				componentId) {
			final int count = sources.size();

			@Override
			public int getMinContextValueCount() {
				return minContextCount;
			}

			@Override
			protected Object[] constructParameters(ComponentEvent event) {
				Object[] parameters = new Object[count];

				for (int i = 0; i < count; i++) {
					parameters[i] = sources.get(i)
							.valueForEventHandlerMethodParameter(event);
				}

				return parameters;
			}
		};
	}

	private EventHandlerMethodParameterSource createEventContextSource(
			final String type, final int parameterIndex) {
		return new EventHandlerMethodParameterSource() {
			public Object valueForEventHandlerMethodParameter(
					ComponentEvent event) {
				return event.coerceContext(parameterIndex, type);
			}
		};
	}

	/**
	 * Returns the component id to match against, or the empty string if the
	 * component id is not specified. The component id is provided by the
	 * OnEvent annotation or (if that is not present) by the part of the method
	 * name following "From" ("onActionFromFoo").
	 */
	private String extractComponentId(String methodName, OnEvent annotation) {
		if (annotation != null)
			return annotation.component();

		// Method name started with "on". Extract the component id, if present.

		int fromx = methodName.indexOf("From");

		if (fromx < 0)
			return "";

		return methodName.substring(fromx + 4);
	}

	/**
	 * Returns the event name to match against, as specified in the annotation
	 * or (if the annotation is not present) extracted from the name of the
	 * method. "onActionFromFoo" or just "onAction".
	 */
	private String extractEventType(String methodName, OnEvent annotation) {
		if (annotation != null)
			return annotation.value();

		int fromx = methodName.indexOf("From");

		// The first two characters are always "on" as in "onActionFromFoo".
		return fromx == -1 ? methodName.substring(2) : methodName.substring(2,
				fromx);
	}
}
